Skip to content

fix(#5448): use field-level merge for validation_loop in base composition - #5450

Open
fullsend-ai-coder[bot] wants to merge 5 commits into
mainfrom
agent/5448-validation-loop-merge
Open

fix(#5448): use field-level merge for validation_loop in base composition#5450
fullsend-ai-coder[bot] wants to merge 5 commits into
mainfrom
agent/5448-validation-loop-merge

Conversation

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

Summary

Fix validation_loop base composition to use field-level merge instead of whole-struct replacement. When a child harness sets only one validation_loop field (e.g., schema), the remaining fields are now inherited from the base, consistent with how env and runner_env merge.

Related Issue

Fixes #5448

Changes

  • Add mergeValidationLoop(base, child *ValidationLoop) helper that merges individual fields (Script, Schema, MaxIterations, FeedbackMode) — child non-zero values win, base fills gaps
  • Replace whole-struct replacement in mergeBaseIntoChild() with field-level merge via the new helper
  • Apply the same field-level merge in mergeForgeConfigInto() (forge-level base composition) and mergeForgeConfig() (forge resolution)
  • Update existing tests and add new test cases covering partial override, full override, nil inheritance, and forge-level merge scenarios

Testing

  • go test -race -cover ./internal/harness/... passes (89.7% coverage)
  • go vet ./internal/harness/... passes
  • Tests added for partial override behavior (child sets only schema, child sets only script)
  • Table-driven unit tests for mergeValidationLoop covering all nil/partial/full combinations
  • Forge-level merge tests for both mergeForgeConfigInto and ResolveForge

Checklist

  • PR title follows Conventional Commits (correct type, ! for breaking changes)
  • No breaking changes — existing behavior when child sets all fields or no fields is preserved

Closes #5448

Post-script verification

  • Branch is not main/master (agent/5448-validation-loop-merge)
  • Secret scan passed (gitleaks — 9525311fa3ff7c81ed791130ee7ce972a71b3eda..HEAD)
  • PR body secret scan passed (gitleaks — no-git)
  • Pre-commit hooks passed (authoritative run on runner)
  • Tests ran inside sandbox

@fullsend-ai-coder
fullsend-ai-coder Bot requested a review from a team as a code owner July 22, 2026 08:51
@fullsend-ai-coder fullsend-ai-coder Bot added the ready-for-review Agent PR ready for human review label Jul 22, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 22, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 8:52 AM UTC · Completed 9:13 AM UTC
Commit: 797a658 · View workflow run →

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Site preview

Preview: https://26109217-site.fullsend-ai.workers.dev

Commit: 36dcd1e1e38d783a6c2e057b2581f813b179f41f

@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

High

  • [adr-inconsistency] internal/harness/compose.go — ADR-0045 (Accepted) documents validation_loop merge as "child replaces base entirely" in three locations (lines 192, 364, 612 of docs/ADRs/0045-forge-portable-harness-schema.md). This PR changes the behavior to field-level merge without updating or superseding the ADR. The user guide (docs/guides/user/bring-your-own-agent.md line 282) also describes the old "replaces entirely" behavior. Per AGENTS.md, changing behavior documented in an Accepted ADR requires writing a new superseding ADR — Accepted ADRs are point-in-time records that must not be substantially rewritten.
    Remediation: Write a new ADR that supersedes ADR-0045's validation_loop merge semantics, or at minimum add a cross-reference annotation to ADR-0045 linking to the new behavior. Update the user guide (docs/guides/user/bring-your-own-agent.md) to describe field-level merge.

Medium

  • [inverse-operation-staleness] internal/harness/diff.go:179DiffHarness uses whole-struct DeepEqual comparison for ValidationLoop (line 179), and diffForgeConfig does the same (line 470). With field-level merge, the diff output is no longer minimal: a child that only changes schema will have the entire ValidationLoop block emitted in the diff, obscuring what was actually customized. The round-trip is still correct (re-merging the non-minimal diff with base reproduces the original), but the diff is unnecessarily verbose.
    Remediation: Update DiffHarness and diffForgeConfig to diff ValidationLoop field-by-field, emitting only non-zero fields that differ from base.

  • [tier-mismatch] internal/harness/compose.go — Issue validation_loop base composition uses whole-struct replacement instead of field-level merge #5448 is labeled "bug" but changes documented ADR-0045 merge semantics. The whole-struct replacement was an intentional design choice documented in an Accepted ADR. Changing it to field-level merge may be a feature or design change rather than a bug fix.

Low

  • [doc-code-mismatch] internal/harness/compose.go:426 — The doc comment on mergeValidationLoop states "If only one is non-nil, returns a copy of it" but the code returns the original pointer, not a copy. The aliasing matches the prior behavior and is harmless in current call sites, but the doc comment is inaccurate.
    Remediation: Fix the doc comment to say "returns it (not a copy)" or change the single-non-nil branches to return a copy.

  • [validation-gap] internal/harness/forge.go:67validateForge requires script when validation_loop is set, but with field-level merge a forge could now set only schema and inherit script from the top level. validateForge runs before ResolveForge and would reject this. Pre-existing issue, not introduced by this PR.

  • [stale-doc] docs/plans/adr-0045-forge-portable-harness-phase1.md:54 — Implementation plan describes old whole-struct replacement behavior at lines 54 and 65.


Labels: PR modifies harness composition logic in internal/harness/

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.


Note: The following inline comments could not be posted on the diff (GitHub returned 422) and are included here instead:

  • internal/harness/compose.go:426: [low] doc-code-mismatch

The doc comment on mergeValidationLoop states 'If only one is non-nil, returns a copy of it' but the code returns the original pointer, not a copy. The aliasing matches prior behavior and is harmless in current call sites, but the doc comment is inaccurate.

Suggested fix: Fix the doc comment to say 'returns it (not a copy)' or change the branches to return a copy.

  • internal/harness/forge.go (file-level): Line 67 · [low] validation-gap

validateForge requires script when validation_loop is set. With field-level merge, a forge could set only schema and inherit script from top level, but validateForge runs before ResolveForge and would reject this. Pre-existing issue, not introduced by this PR.

@fullsend-ai-review fullsend-ai-review Bot added the component/harness Agent harness, config, and skills loading label Jul 22, 2026
@rh-hemartin

Copy link
Copy Markdown
Member

/fs-fix

@fullsend-ai-coder

fullsend-ai-coder Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 10:15 AM UTC · Completed 10:20 AM UTC
Commit: 7ada4e0 · View workflow run →

fullsend-ai-coder Bot added a commit that referenced this pull request Jul 22, 2026
The comment claimed single-non-nil returns produced a copy, but the
code returns the original pointer. Update the comment to say
"returns it as-is" to match actual behavior.

Addresses review feedback on #5450
@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 Fix agent — iteration 1 (human-triggered)

Addressed 1 of 2 review findings. Fixed the doc-code mismatch on mergeValidationLoop. Disagreed with 1 finding (pre-existing validation ordering issue, out of scope for this PR).

Fixed (1):

  1. doc-code-mismatch: mergeValidationLoop comment says 'returns a copy' but returns original pointer (internal/harness/compose.go): Updated doc comment from 'returns a copy of it' to 'returns it as-is' to accurately describe the single-non-nil return behavior.

Disagreed (1):

  1. validation-gap: validateForge requires script when validation_loop is set, but forge could set only schema and inherit script from top level: Reviewer explicitly notes this is a pre-existing issue not introduced by this PR. Fixing the validation ordering (validateForge runs before ResolveForge) is out of scope for this PR and would change validation semantics beyond the field-level merge feature.

Tests: passed

Updated by fullsend fix agent

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated multi-agent review pass (3 independent reviewer agents, findings cross-verified against the PR head branch). 4 findings survived triage at MEDIUM+ severity: 1 HIGH, 3 MEDIUM. Summary:

  • A DiffHarness/migrate-customizations round-trip regression: the merge side moved to field-level semantics but the diff side (inverse) is still whole-struct, so re-composing a diffed-out child can silently backfill fields from base that the child intentionally left empty.
  • ADR-0045 still documents the old whole-struct-replace rule for validation_loop in three places and wasn't updated alongside this behavior change.
  • An explicit validation_loop: {} now silently and fully inherits the base block (bypassing the previous "script required" load error) as a side effect of zero-value-as-unset semantics, resolving an ADR-0045 open question without documenting or testing it.
  • The forge-resolution call site's field-level merge (ResolveForge/mergeForgeConfig) can't actually be exercised through any real YAML-loading path due to a pre-existing validateForge guard, and the new unit test for it bypasses that guard rather than proving the real path works.

See inline comments for file/line detail and suggested fixes. No LOW/INFO findings are included per review policy.

// mergeValidationLoop merges two ValidationLoop pointers field by field.
// Child non-zero values win; base fills gaps. Returns the merged result.
// If both are nil, returns nil. If only one is non-nil, returns it as-is.
func mergeValidationLoop(base, child *ValidationLoop) *ValidationLoop {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] — DiffHarness was not updated to match the new field-level merge, breaking the base/child round-trip used by migrate-customizations

ADR-0045's Consequences section states the base: merge rules and their inverse (DiffHarness) must change together ("Changes to merge rules must be reflected in both directions"), but this PR only updates the merge side. DiffHarness (internal/harness/diff.go:179-186) and diffForgeConfig (diff.go:470-473) still treat ValidationLoop as whole-struct: if child.ValidationLoop != nil && !reflect.DeepEqual(child.ValidationLoop, base.ValidationLoop) { result.Child.ValidationLoop = child.ValidationLoop }. When old whole-struct-replace merge was in effect, copying the entire child struct into the diff output was correct. With the new field-level mergeValidationLoop, it's not: if the child's ValidationLoop differs from base in just one field (say Script) while another field (say Schema) is genuinely empty/unset for that child, the whole child struct — including that genuine empty Schema — gets copied into the "minimal" diff. Re-composing that diffed-out child against the same base via mergeBaseIntoChild then backfills Schema from base, silently changing effective validation config.

This is reachable in production: internal/cli/migrate.go:479 calls harness.DiffHarness(&upstreamHarness, &customizedHarness, ...) as the core of the migrate-customizations command (ADR-0064), which converts standalone customized harnesses into base:-composed ones — exactly the diff → later-remerge path described above.

Suggestion: Diff ValidationLoop field-by-field in DiffHarness/diffForgeConfig (mirroring mergeValidationLoop's field set), consistent with how RunnerEnv/Env are already diffed field-by-field elsewhere in the same file. Add a round-trip test where base sets schema/feedback_mode and the differing child intentionally leaves them zero.

(Independently identified by 2 of 3 review agents, both verified by tracing the actual code paths.)

// - Slices (skills, plugins, providers, api_servers): base + child (concatenated)
// - Maps (runner_env): base merged with child; child keys win
// - Pointer structs (validation_loop, security): child replaces if non-nil
// - Pointer structs (validation_loop): field-level merge; child non-zero fields win

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] premature-decision — ADR-0045 still documents whole-struct replacement for validation_loop; not updated alongside this behavior change

This comment (and its counterpart in forge.go's mergeForgeConfig doc) says the merge follows "ADR-0045 merge rules," but docs/ADRs/0045-forge-portable-harness-schema.md wasn't touched by this PR and still documents the old rule in three places: the forge inheritance table ("validation_loop | Forge value replaces top-level value entirely"), the base-composition merge-rules list ("validation_loop: child replaces base entirely (if non-nil)"), and the Open Questions section (still reasons about validation_loop on the premise "non-nil (including zero struct) = override entirely"). The ADR is the authoritative source these code comments explicitly cite; leaving it stale means the documented architecture no longer matches the code, and the ADR's own nil-vs-empty open question for validation_loop is left looking unresolved when this PR actually changes that behavior (see the other comment on this file re: validation_loop: {}).

Suggestion: Amend ADR-0045's inheritance table, base-merge-rules list, and open-questions note for validation_loop to describe the new field-level merge, in this PR or a fast-follow.

(Flagged independently by 2 of 3 review agents; both quoted the exact stale ADR passages.)

Comment thread internal/harness/compose.go
Comment thread internal/harness/forge_test.go
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.49123% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/harness/diff.go 93.54% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@rh-hemartin

Copy link
Copy Markdown
Member

/fs-fix

@fullsend-ai-coder

fullsend-ai-coder Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 9:21 AM UTC · Completed 9:35 AM UTC
Commit: 7ada4e0 · View workflow run →

fullsend-ai-coder Bot added a commit that referenced this pull request Jul 23, 2026
Address review findings from waynesun09 on PR #5450:

- DiffHarness/diffForgeConfig: diff ValidationLoop field-by-field
  (matching mergeValidationLoop semantics) so the diff→merge round-trip
  used by migrate-customizations is consistent. Add diffValidationLoop
  helper and round-trip tests.

- mergeValidationLoop: special-case an all-zero child (validation_loop: {})
  to return it as-is, preserving the pre-field-merge "script required"
  rejection instead of silent full inheritance from base.

- validateForge: allow forge validation_loop without script when the
  top-level harness has validation_loop.script set (the script will be
  inherited via field-level merge in ResolveForge). Add LoadWithOpts
  end-to-end tests proving the real loading path works.

Addresses review feedback on #5450
@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 Fix agent — iteration 2 (human-triggered)

Addressed 3 of 4 review findings from waynesun09. Fixed DiffHarness/diffForgeConfig round-trip regression (HIGH), added zero-value guard for mergeValidationLoop (MEDIUM), and relaxed validateForge to make forge field-level merge reachable through real loading paths (MEDIUM). Disagreed with ADR-0045 update (MEDIUM) per AGENTS.md immutability policy for Accepted ADRs.

Fixed (3):

  1. DiffHarness not updated to match field-level merge, breaking migrate-customizations round-trip (internal/harness/diff.go): Replaced whole-struct diff with field-by-field diffValidationLoop helper in both DiffHarness and diffForgeConfig. Added round-trip test proving diff-then-mergeBaseIntoChild reproduces the original child when only some ValidationLoop fields differ.
  2. zero-value-as-unset semantics let validation_loop: {} silently inherit base (internal/harness/compose.go): Added all-zero child special case in mergeValidationLoop: when child is non-nil but all fields are zero-valued, it is returned as-is so Validate rejects it with script-is-required, preserving pre-field-merge behavior. Added TestMergeValidationLoop_AllZeroChild test.
  3. field-level merge for forge-resolution unreachable through real YAML-loading path due to validateForge guard (internal/harness/forge.go): Relaxed validateForge to allow empty forge-level validation_loop.script when top-level harness has validation_loop.script set. Added LoadWithOpts end-to-end tests proving the real loading path works and rejection is preserved when no top-level script exists.

Disagreed (1):

  1. ADR-0045 still documents whole-struct replacement for validation_loop: AGENTS.md policy prohibits substantial rewrites of Accepted ADRs. Updating the merge-rules table, inheritance table, and open-questions section would be a substantial rewrite. The correct approach per policy is a new ADR that supersedes ADR-0045, or a minor cross-reference annotation. This should be a separate follow-up.

Tests: passed

Updated by fullsend fix agent

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of 36dcd1e1e against the 4 findings from the previous round.

Resolved (threads closed):

  • The validation_loop: {} full-inheritance bypass (base composition) is fixed — mergeValidationLoop now special-cases a non-nil-but-all-zero child so Validate()'s "script is required" check still rejects it, matching pre-field-merge behavior.
  • The forge-resolution field-level-merge reachability gap is fixed — validateForge now allows an empty forge-level script when the top level has one, and a real LoadWithOpts end-to-end test proves the partial-override path actually works, not just the helper in isolation.

Still open:

  • ADR-0045 is untouched by this commit and still documents whole-struct validation_loop replacement in multiple places (inheritance table, base-merge rules list, Open Questions).
  • The DiffHarness round-trip fix below is real progress but doesn't fully close the gap — see the follow-up comment on the round-trip finding.

New (introduced by this commit):

  • The two fixes above don't compose cleanly at the forge level — see the new comment on compose.go.

Comment thread internal/harness/diff.go
// equal, or child is nil (removal is handled by the caller). This mirrors
// mergeValidationLoop's field set so diff→merge round-trips correctly for
// non-zero field values.
func diffValidationLoop(base, child *ValidationLoop) *ValidationLoop {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] — Follow-up on the round-trip finding above (still open): the fix narrows but does not close the gap

36dcd1e1e added diffValidationLoop and switched DiffHarness/diffForgeConfig to field-level diff, matching the merge side's granularity. That closes the specific whole-struct-vs-field-level mismatch, but the underlying round-trip problem is only partially fixed.

diffValidationLoop and mergeValidationLoop both use the Go zero value as the only signal for "not set, inherit from base." They can't distinguish "this field equals base's value, omit it from the diff" from "this field's real value is the zero value, and it differs from a non-zero base value." Concretely: base = {Script:"base.sh", Schema:"base-schema.json", MaxIterations:5, FeedbackMode:"append"}; a customized child overrides Script and intentionally carries no schema: child = {Script:"custom.sh", Schema:"", MaxIterations:5, FeedbackMode:"append"}. diffValidationLoop(base, child) produces {Script:"custom.sh", Schema:"", MaxIterations:0, FeedbackMode:""}. Feeding that back through mergeValidationLoop(base, diff) yields Schema:"base-schema.json" — silently reintroducing schema validation the customized harness never wanted. Still reachable via migrate-customizations (internal/cli/migrate.go's DiffHarness call). The new round-trip tests (TestDiffHarness_ValidationLoopFieldLevelRoundTrip, TestDiffForgeConfig_ValidationLoopFieldLevel) only exercise children whose non-overridden fields already equal base's, so they pass without covering this.

Suggestion: A zero-value-comparison diff/merge can't express "explicitly zero, differs from non-zero base." Track field presence explicitly (pointer fields, or a presence map/bitmask alongside ValidationLoop) instead of inferring it from the value, or explicitly document this as an accepted migrate-customizations limitation and add a test that locks in the (documented) behavior rather than leaving it to silently regress.

(Independently confirmed — with an identical empirical repro — by all 4 review agents in this round.)


// Non-nil but all-zero child = explicit empty override (e.g., `validation_loop: {}`).
// Return as-is to preserve the pre-field-merge rejection behavior.
if *child == (ValidationLoop{}) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] — New in 36dcd1e1e: this all-zero-child special case (added to fix the validation_loop: {} finding) breaks forge-level inheritance, contradicting validateForge's own comment

This commit's two fixes don't compose. This special case (if *child == (ValidationLoop{}) { return child }) is correct for the base→child composition path it was written for, but mergeValidationLoop is shared by every caller, including mergeForgeConfig (forge.go) and mergeForgeConfigInto (compose.go), where a non-nil-but-all-zero forge-level block should mean "no forge-specific override, inherit everything" — matching how skills/runner_env already treat an empty forge override — not "explicit override, reject."

Concretely, this breaks the exact scenario validateForge's new topHasScript relaxation was written to support: a fully-populated top-level validation_loop plus forge.<platform>.validation_loop: {} (a literal empty mapping, which unmarshals to a non-nil pointer to an all-zero struct, not nil). validateForge lets this load, on the comment's own promise that "the script will be inherited via field-level merge in ResolveForge" — but ResolveForgemergeForgeConfig hits this special case and returns the empty struct as-is, discarding the top-level Script/Schema/MaxIterations/FeedbackMode entirely. Validate() then rejects the load with validation_loop.script is required when validation_loop is set — a confusing failure for a config validateForge itself just approved, on the very promise that just got broken. Confirmed end-to-end via LoadWithOpts; the same defect reproduces through mergeForgeConfigInto's base-chain forge composition. Neither new test (TestLoadWithOpts_ForgePartialValidationLoopInheritsScript, TestLoadWithOpts_ForgeEmptyValidationLoopNoTopLevelScript) covers a fully-empty forge block with a populated top level, so this shipped uncaught.

Suggestion: Move the all-zero-child "reject" special case out of the shared helper and into the mergeBaseIntoChild call site specifically, or add a parameter so mergeForgeConfig/mergeForgeConfigInto can opt out and fall through to normal field-by-field backfill for an all-zero child. Add end-to-end coverage for "top-level populated + forge validation_loop: {}" on both the ResolveForge and LoadWithBase paths.

(Independently confirmed — across the ResolveForge and base-chain forge-composition call sites — by all 4 review agents in this round.)

@rh-hemartin

Copy link
Copy Markdown
Member

/fs-fix

@fullsend-ai-coder

fullsend-ai-coder Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 6:51 AM UTC · Completed 6:57 AM UTC
Commit: 36dcd1e · View workflow run →

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 Fix agent — iteration 3 (human-triggered)

Both review findings are already addressed. Finding 1 (doc-code-mismatch) was fixed in commit 05b2a68 — the doc now accurately says 'returns it as-is'. Finding 2 (validation-gap) was noted as pre-existing by the reviewer and is already fixed in commit 36dcd1e with the topHasScript guard. All tests pass (go test -race, go vet clean). No new commit needed.

Disagreed (2):

  1. doc-code-mismatch: mergeValidationLoop doc says 'returns a copy' but code returns original pointer: Already fixed in prior iteration (commit 05b2a68). The current doc comment at compose.go:426 reads 'If only one is non-nil, returns it as-is' which accurately describes the code returning the original pointer. The review appears to reference the pre-fix wording.
  2. validation-gap: validateForge rejects forge validation_loop without script before ResolveForge can inherit it: The review itself notes this is a 'Pre-existing issue, not introduced by this PR.' Moreover, the PR already addresses it (forge.go:67-70) with the topHasScript check added in commit 36dcd1e, allowing empty forge script when the top-level harness has a script to inherit. End-to-end test TestLoadWithOpts_ForgePartialValidationLoopInheritsScript confirms this works.

Tests: passed

Updated by fullsend fix agent

@rh-hemartin

Copy link
Copy Markdown
Member

/fs-fix

@fullsend-ai-coder

fullsend-ai-coder Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 7:21 AM UTC · Completed 7:26 AM UTC
Commit: 36dcd1e · View workflow run →

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 Fix agent — iteration 3 (human-triggered)

Both [low] review findings are already resolved by prior fix iterations. Finding 1 (doc-code-mismatch): the doc comment was corrected in commit 05b2a68 and now accurately says 'returns it as-is'. Finding 2 (validation-gap): the PR already includes a topHasScript check in validateForge (forge.go:67-70) with an end-to-end test. All tests pass (go test -race). No code changes required.

Disagreed (2):

  1. doc-code-mismatch in mergeValidationLoop comment: The reviewer claims the doc states 'returns a copy of it', but the actual doc comment (line 426 of compose.go) reads 'If only one is non-nil, returns it as-is.' — which accurately describes the pointer-returning behavior. The doc was already corrected in a prior fix iteration (commit 05b2a68). No change needed.
  2. validation-gap in validateForge for forge validation_loop without script: The reviewer describes this as a 'pre-existing issue, not introduced by this PR.' However, this PR already addresses it: forge.go lines 67-70 add a topHasScript check that allows an empty forge script when the top-level harness has a script that will be inherited via field-level merge. The end-to-end test TestLoadWithOpts_ForgePartialValidationLoopInheritsScript proves the full loading path (validateForge → ResolveForge → Validate) works correctly. No change needed.

Tests: passed

Updated by fullsend fix agent

@rh-hemartin

Copy link
Copy Markdown
Member

/fs-fix rebase and fix conflicts

@fullsend-ai-coder

fullsend-ai-coder Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 12:55 PM UTC · Completed 1:04 PM UTC
Commit: 36dcd1e · View workflow run →

…tion

The mergeBaseIntoChild() function used whole-struct replacement for
ValidationLoop: if the child's pointer was non-nil (which happens
when any sub-field is set in YAML), the base's entire ValidationLoop
was discarded. This caused validation failures when a child harness
set only validation_loop.schema — the base's script, max_iterations,
and feedback_mode were lost.

Add a mergeValidationLoop() helper that merges individual fields
(Script, Schema, MaxIterations, FeedbackMode) where child non-zero
values win and base fills gaps. Apply the same field-level merge in
mergeForgeConfigInto() (base composition of forge configs) and
mergeForgeConfig() (forge resolution).

Note: golangci-lint was not available in the sandbox. go vet passed.

Closes #5448
The comment claimed single-non-nil returns produced a copy, but the
code returns the original pointer. Update the comment to say
"returns it as-is" to match actual behavior.

Addresses review feedback on #5450
Address review findings from waynesun09 on PR #5450:

- DiffHarness/diffForgeConfig: diff ValidationLoop field-by-field
  (matching mergeValidationLoop semantics) so the diff→merge round-trip
  used by migrate-customizations is consistent. Add diffValidationLoop
  helper and round-trip tests.

- mergeValidationLoop: special-case an all-zero child (validation_loop: {})
  to return it as-is, preserving the pre-field-merge "script required"
  rejection instead of silent full inheritance from base.

- validateForge: allow forge validation_loop without script when the
  top-level harness has validation_loop.script set (the script will be
  inherited via field-level merge in ResolveForge). Add LoadWithOpts
  end-to-end tests proving the real loading path works.

Addresses review feedback on #5450
After rebasing onto main (which added the PreflightCheck field to
ValidationLoop), update diffValidationLoop to include PreflightCheck
in field-level diffs, matching mergeValidationLoop's field set.
Also adds PreflightCheck test cases to TestMergeValidationLoop.

Addresses review feedback on #5450
@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 Fix agent — iteration 3 (human-triggered)

Rebased PR onto origin/main, resolving conflicts in compose.go and compose_test.go. Integrated the new PreflightCheck field (added on main) into the mergeValidationLoop and diffValidationLoop helpers. All tests pass.

Fixed (1):

  1. rebase and fix conflicts: Rebased 3 commits onto origin/main, resolving merge conflicts in compose.go and compose_test.go. Main had added PreflightCheck carry-forward logic to mergeBaseIntoChild and mergeForgeConfigInto; our mergeValidationLoop helper now handles PreflightCheck as part of its comprehensive field-level merge, superseding the special-case logic. Kept both main's PreflightCheck-specific tests and our field-level merge tests. Added PreflightCheck test cases to TestMergeValidationLoop table-driven test. Also added PreflightCheck handling to diffValidationLoop to keep diff and merge in sync.

Tests: passed

Decision points
  • Main's PreflightCheck carry-forward logic vs our mergeValidationLoop helper (alternatives: Keep main's special-case PreflightCheck logic alongside mergeValidationLoop, Integrate PreflightCheck into mergeValidationLoop as another field; rationale: mergeValidationLoop already handles all other ValidationLoop fields with the same pattern (child non-zero wins, base fills gaps). Adding PreflightCheck to it is the natural extension and eliminates the need for separate special-case code in mergeBaseIntoChild and mergeForgeConfigInto.)

Updated by fullsend fix agent

@rh-hemartin

Copy link
Copy Markdown
Member

/fs-fix increase the code coverage

@fullsend-ai-coder

fullsend-ai-coder Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 9:53 AM UTC · Completed 10:02 AM UTC
Commit: 47ba571 · View workflow run →

Add targeted tests to increase coverage of PR-changed code:

- TestDiffValidationLoop: table-driven tests covering all code paths
  (both nil, base nil, child nil, equal, schema/feedbackMode/preflight
  diffs, multiple field diffs) — raises diffValidationLoop from 65.4%
  to 96.2%
- TestDiffForgeConfig_*: covers child nil, PostScript diff, RunnerEnv
  removal, and Env removal — raises diffForgeConfig from 84.4% to 93.8%
- TestMergeForgeConfigInto_RunnerEnvInherit/EnvInherit: covers RunnerEnv
  and Env inheritance when child has nil values — raises
  mergeForgeConfigInto from 78.9% to 100%
- TestResolveForge_EnvMergeNilHarnessEnv: covers Env merge when harness
  Env is nil — raises mergeForgeConfig from 93.8% to 100%

Overall harness package coverage: 89.4% → 90.5%

Addresses review feedback on #5450
@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 Fix agent — iteration 4 (human-triggered)

Added 11 new test functions (251 lines) targeting the largest coverage gaps in PR-changed code. Key improvements: diffValidationLoop 65.4%→96.2%, mergeForgeConfigInto 78.9%→100%, mergeForgeConfig 93.8%→100%, diffForgeConfig 84.4%→93.8%. Overall harness package coverage improved from 89.4% to 90.5%.

Fixed (1):

  1. increase the code coverage: Added direct unit tests for uncovered helper functions: TestDiffValidationLoop (table-driven, 7 cases covering all code paths), TestDiffForgeConfig_ChildNil/PostScriptDiff/RunnerEnvRemoval/EnvRemoval, TestMergeForgeConfigInto_RunnerEnvInherit/EnvInherit, TestResolveForge_EnvMergeNilHarnessEnv. Raised diffValidationLoop from 65.4% to 96.2%, mergeForgeConfigInto from 78.9% to 100%, mergeForgeConfig from 93.8% to 100%, diffForgeConfig from 84.4% to 93.8%. Overall harness package coverage: 89.4% to 90.5%.

Tests: passed

Strategy change: Switched from integration tests (prior iterations) to direct unit tests of unexported helper functions for higher coverage impact per test.

Decision points
  • Chose direct unit tests for unexported helper functions over additional integration tests via LoadWithBase/DiffHarness (alternatives: More integration tests through LoadWithBase, Coverage via end-to-end harness loading; rationale: FIX_ITERATION=4 exceeds escalation threshold of 3. Prior iterations added integration tests; this iteration takes a fundamentally different approach by testing unexported helpers directly for maximum coverage improvement per test added.)

Updated by fullsend fix agent

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/harness Agent harness, config, and skills loading ready-for-review Agent PR ready for human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

validation_loop base composition uses whole-struct replacement instead of field-level merge

2 participants